﻿using System;
using ProjectAutumn.Graphics;
using ProjectAutumn.Input;
using ProjectAutumn.Window;

namespace ProjectAutumn
{
    public abstract class Game : IDisposable
    {
        private readonly GameClock clock = new GameClock();
        private readonly GameTime intGameTime = new GameTime();
        private readonly TimeSpan maximumElapsedTime = TimeSpan.FromMilliseconds(500.0);
        private TimeSpan accumulatedElapsedGameTime;
        private bool drawRunningSlowly;
        private bool first = true;
        private bool forceElapsedTimeToZero;
        private TimeSpan lastFrameElapsedGameTime;
        private TimeSpan lastFrameElapsedRealTime;
        private long lastUpdateFrame;
        private float lastUpdateTime;
        private TimeSpan targetElapsedTime = TimeSpan.FromTicks(166667);
        private TimeSpan totalGameTime;
        private int updatesSinceRunningSlowly1 = int.MaxValue;
        private int updatesSinceRunningSlowly2 = int.MaxValue;
        public event EventHandler Exiting;
        private void OnExit(EventArgs e)
        {
            EventHandler handler = Exiting;
            if (handler != null) handler(this, e);
        }
        protected Game() { IsFixedTimeStep = true; }
        #region IGame Members
        public bool IsFixedTimeStep { get; set; }
        public bool IsExiting { get; private set; }
        public TimeSpan TargetElapsedTime
        {
            get { return targetElapsedTime; }
            set
            {
                if (value <= TimeSpan.Zero)
                    throw new ArgumentOutOfRangeException("value", "Target elapsed time must be greater than zero.");
                targetElapsedTime = value;
            }
        }
        public WindowDevice WindowDevice { get; private set; }
        public GraphicsDevice GraphicsDevice { get; private set; }
        public InputDevice InputDevice { get; private set; }
        public WindowSystems WindowSystem { get; private set; }
        public GraphicsSystems GraphicsSystem { get; private set; }
        public InputSystems InputSystem { get; private set; }
        public virtual void Dispose()
        {
            IsExiting = true;
            InputDevice.Dispose();
            GraphicsDevice.Dispose();
            WindowDevice.Dispose();
            OnExit(new EventArgs());
        }
        public void Run(WindowSystems window, GraphicsSystems graphics, InputSystems input)
        {
            WindowSystem = window;
            GraphicsSystem = graphics;
            InputSystem = input;
            WindowDevice = WindowManager.CreateWindow(window, this);
            if (WindowDevice == null) throw new Exception("Error creating window");
            GraphicsDevice = GraphicsManager.CreateGraphics(graphics, this);
            if (GraphicsDevice == null) throw new Exception("Error creating Graphics Device");
            InputDevice = InputManager.CreateInput(input, this);
            if (InputDevice == null) throw new Exception("Error creating Input Device");
            WindowDevice.Run();
        }
        public void Tick()
        {
            if (IsExiting)
            {
                Dispose();
                return;
            }
            if (first)
            {
                FirstUpdate();
                first = false;
            }
            clock.Step();
            intGameTime.TotalRealTime = (float)clock.CurrentTime.TotalSeconds;
            intGameTime.ElapsedRealTime = (float)clock.ElapsedTime.TotalSeconds;
            lastFrameElapsedRealTime += clock.ElapsedTime;
            TimeSpan elapsedAdjustedTime = clock.ElapsedAdjustedTime;
            if (elapsedAdjustedTime < TimeSpan.Zero)
                elapsedAdjustedTime = TimeSpan.Zero;
            if (forceElapsedTimeToZero)
            {
                intGameTime.ElapsedRealTime = 0;
                lastFrameElapsedRealTime = elapsedAdjustedTime = TimeSpan.Zero;
                forceElapsedTimeToZero = false;
            }
            if (elapsedAdjustedTime > maximumElapsedTime)
                elapsedAdjustedTime = maximumElapsedTime;
            if (IsFixedTimeStep)
            {
                accumulatedElapsedGameTime += elapsedAdjustedTime;
                long ratio = accumulatedElapsedGameTime.Ticks / TargetElapsedTime.Ticks;
                accumulatedElapsedGameTime =
                        TimeSpan.FromTicks(accumulatedElapsedGameTime.Ticks % TargetElapsedTime.Ticks);
                lastFrameElapsedGameTime = TimeSpan.Zero;
                if (ratio == 0)
                    return;
                TimeSpan span = TargetElapsedTime;
                if (ratio > 1)
                {
                    updatesSinceRunningSlowly2 = updatesSinceRunningSlowly1;
                    updatesSinceRunningSlowly1 = 0;
                }
                else
                {
                    if (updatesSinceRunningSlowly1 < int.MaxValue)
                        updatesSinceRunningSlowly1++;
                    if (updatesSinceRunningSlowly2 < int.MaxValue)
                        updatesSinceRunningSlowly2++;
                }
                drawRunningSlowly = updatesSinceRunningSlowly2 < 20;
                while (ratio > 0 &&
                      !IsExiting)
                {
                    ratio -= 1;
                    try
                    {
                        intGameTime.ElapsedGameTime = (float)span.TotalSeconds;
                        intGameTime.TotalGameTime = (float)totalGameTime.TotalSeconds;
                        intGameTime.IsRunningSlowly = drawRunningSlowly;
                        Update(intGameTime);
                    }
                    finally
                    {
                        lastFrameElapsedGameTime += span;
                        totalGameTime += span;
                    }
                }
            }
            else
            {
                drawRunningSlowly = false;
                updatesSinceRunningSlowly1 = int.MaxValue;
                updatesSinceRunningSlowly2 = int.MaxValue;
                if (!IsExiting)
                {
                    try
                    {
                        intGameTime.ElapsedGameTime = 0;
                        lastFrameElapsedGameTime = elapsedAdjustedTime;
                        intGameTime.TotalGameTime = (float)totalGameTime.TotalSeconds;
                        intGameTime.IsRunningSlowly = false;
                        Update(intGameTime);
                    }
                    finally
                    {
                        totalGameTime += elapsedAdjustedTime;
                    }
                }
            }
            DrawFrame();
            lastUpdateFrame++;
            if ((float)clock.CurrentTime.TotalSeconds - lastUpdateTime <= 1.0f) return;
            intGameTime.FramesPerSecond = lastUpdateFrame / (float)(clock.CurrentTime.TotalSeconds - lastUpdateTime);
            lastUpdateTime = (float)clock.CurrentTime.TotalSeconds;
            lastUpdateFrame = 0;
        }
        public void Exit()
        {
            IsExiting = true;
        }
        public virtual void Draw(GameTime gameTime) { }
        public virtual void Initialize() { }
        public virtual void Update(GameTime gameTime) { }
        #endregion
        private void FirstUpdate()
        {
            WindowDevice.Title = "Darcade Game";
            GraphicsDevice.ApplyChanges();
            if (GraphicsDevice.GraphicCapabilities.SupportsChangingMouseVisibility)
                GraphicsDevice.IsMouseVisible = false;
            Initialize();
        }
        private void DrawFrame()
        {
            try
            {
                if (!IsExiting)
                {
                    intGameTime.TotalRealTime = (float)clock.CurrentTime.TotalSeconds;
                    intGameTime.ElapsedRealTime = (float)lastFrameElapsedRealTime.TotalSeconds;
                    intGameTime.TotalGameTime = (float)totalGameTime.TotalSeconds;
                    intGameTime.ElapsedGameTime = (float)lastFrameElapsedGameTime.TotalSeconds;
                    intGameTime.IsRunningSlowly = drawRunningSlowly;
                    GraphicsDevice.BeginDraw();
                    Draw(intGameTime);
                    GraphicsDevice.EndDraw();
                }
            }
            finally
            {
                lastFrameElapsedGameTime = TimeSpan.Zero;
                lastFrameElapsedRealTime = TimeSpan.Zero;
            }
        }
        internal void WindowApplyChanges(int width, int height, bool isFullscreen)
        {
            WindowDevice.ApplyChanges(width, height, isFullscreen);
        }
    }
}
